﻿/* Author: Achim Schnell */

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

namespace AppFx.Data.Sort
{
	/// <summary>
	/// This class can sort lists of objects
	/// </summary>
	public class ObjectComparer : IComparer, IComparer<object>
	{
		#region Properties

		/// <summary>
		/// Enthält die anzuwendenden Sortierungen.
		/// </summary>
		public ListSortDescriptionCollection Sorts{get; protected set;}

		#endregion

		#region Constructors

		/// <summary>
		/// Erstellt eine neue Instanz der <see cref="ObjectComparer"/>-Klasse.
		/// </summary>
		public ObjectComparer() : this(null) { }

		/// <summary>
		/// Erstellt eine neue Instanz der <see cref="ObjectComparer"/>-Klasse.
		/// </summary>
		/// <param name="possibleDescriptors">Die möglichen PropertyDescriptors.</param>
		/// <param name="sortExpression">Der Sortierausdruck.</param>
		/// <example>
		/// Ausdrucksschema für <see cref="sortExpression"/>:
		/// Klassen-Eigenschaften: Title, Name, LastName, Zweiteiliger Name
		/// 
		/// Schema:
		/// Title -> Sortiert aufsteigend nach Titel
		/// Title, Name -> Sortiert aufsteigend nach Titel, anschließend nach Name
		/// Title, Name DESC -> Sortiert aufsteigend nach Title, anschließend absteigend nach Name
		/// [Zweiteiliger Name]
		/// </example>
		public ObjectComparer(IEnumerable<PropertyDescriptor> possibleDescriptors, string sortExpression)
			: this(ObjectComparer.ParseSortExpression(possibleDescriptors, sortExpression)) { }

		/// <summary>
		/// Erstellt eine neue Instanz der <see cref="ObjectComparer"/>-Klasse.
		/// </summary>
		/// <param name="sorts">Die anzuwendenen Sortierungen.</param>
		public ObjectComparer(ListSortDescriptionCollection sorts)
		{
			if (sorts == null)
				throw new ArgumentNullException("sorts");
			Sorts = sorts;
		}

		#endregion

		#region Methods

		public static ListSortDescriptionCollection ParseSortExpression(IEnumerable<PropertyDescriptor> possibleDescriptors, string sortExpression)
		{
			if (possibleDescriptors == null)
				throw new ArgumentNullException("possibleDescriptors");
			if (String.IsNullOrWhiteSpace(sortExpression))
				throw new ArgumentNullException("sortString");
			string[] parts = sortExpression.Split(new char[] {','}, StringSplitOptions.RemoveEmptyEntries);
			if (parts.Length > 0)
			{
				List<ListSortDescription> sorts = new List<ListSortDescription>();
				for(int i = 0; i < parts.Length; i++)
				{
					ListSortDirection direction = ListSortDirection.Ascending;
					parts[i] = parts[i].Trim();
					if (parts[i].ToUpper().EndsWith("DESC"))
						direction = ListSortDirection.Descending;
					string propName = String.Empty;
					if (parts[i].Contains(" "))
						propName = parts[i].Substring(0, parts[i].LastIndexOf(" "));
					else
						propName = parts[i];
					var props = possibleDescriptors.Where(p => p.Name == propName);
					if (props == null || props.Count() == 0)
						throw new ArgumentException(String.Format("The Parser wasn't able to find the property {0} in the given descriptors.", propName));
					sorts.Add(new ListSortDescription(props.First(), direction));
				}
				return new ListSortDescriptionCollection(sorts.ToArray());
			}
			else
				return new ListSortDescriptionCollection();
		}

		/// <summary>
		/// Vergleicht zwei Objekte und gibt einen Wert zurück, der angibt, ob eines der Objekte kleiner, gleich oder größer als das andere Objekt ist.
		/// </summary>
		/// <param name="x">Das erste zu vergleichende Objekt.</param>
		/// <param name="y">Das zweite zu vergleichende Objekt.</param>
		/// <returns>
		/// Eine ganze Zahl mit Vorzeichen, die die relativen Werte von <paramref name="x"/> und <paramref name="y"/> angibt, wie in der folgenden Tabelle veranschaulicht.Wert Bedeutung Kleiner als 0 <paramref name="x"/> ist kleiner als <paramref name="y"/>. Zero <paramref name="x"/> ist gleich <paramref name="y"/>. Größer als 0 <paramref name="x"/> ist größer als <paramref name="y"/>.
		/// </returns>
		/// <exception cref="T:System.ArgumentException">Weder <paramref name="x"/> noch <paramref name="y"/> implementieren die <see cref="T:System.IComparable"/>-Schnittstelle.– oder – <paramref name="x"/> und <paramref name="y"/> sind nicht vom gleichen Typ. Keines der beiden kann einen Vergleich mit dem jeweils anderen Objekt behandeln. </exception>
		public int Compare(object x, object y)
		{
			return Sorts.Count > 0 ? CompareLevel(0, x, y) : 0;
		}

		/// <summary>
		/// Vergleicht die übergebenen Objekte miteinander unter Berücksichtigung der aktuellen Sortierung, sowie deren Nachfolger.
		/// </summary>
		/// <param name="level">Aktueller Sortierungslevel.</param>
		/// <param name="x">Objekt X.</param>
		/// <param name="y">Objekt Y.</param>
		/// <returns>
		/// Eine ganze Zahl mit Vorzeichen, die die relativen Werte von <paramref name="x"/> und <paramref name="y"/> angibt, wie in der folgenden Tabelle veranschaulicht.Wert Bedeutung Kleiner als 0 <paramref name="x"/> ist kleiner als <paramref name="y"/>. Zero <paramref name="x"/> ist gleich <paramref name="y"/>. Größer als 0 <paramref name="x"/> ist größer als <paramref name="y"/>.
		/// </returns>
		protected virtual int CompareLevel(int level, object x, object y)
		{
			if (Sorts.Count - 1 >= level)
			{
				if (x == null && y != null)
					return -1;
				else if (x != null && y == null)
					return 1;
				else if (x == null && y == null)
					return 0;
				else
				{
					object xProperty = Sorts[level].PropertyDescriptor.GetValue(x);
					object yProperty = Sorts[level].PropertyDescriptor.GetValue(y);
					return ComparePropertyLevel(level, xProperty, yProperty, x, y);
				}
			}
			else
			{
				return 0;
			}
		}

		/// <summary>
		/// Vergleicht die übergebenen Objekte miteinander unter Berücksichtigung der angegeben Sortierung, sowie deren Nachfolger.
		/// </summary>
		/// <param name="level">Aktueller Sortierungslevel.</param>
		/// <param name="x">Objekt X.</param>
		/// <param name="y">Objekt Y.</param>
		/// <param name="xParent">Komponente des Objekt X.</param>
		/// <param name="yParent">Komponente des Objekt Y.</param>
		/// <returns>
		/// Eine ganze Zahl mit Vorzeichen, die die relativen Werte von <paramref name="x"/> und <paramref name="y"/> angibt, wie in der folgenden Tabelle veranschaulicht.Wert Bedeutung Kleiner als 0 <paramref name="x"/> ist kleiner als <paramref name="y"/>. Zero <paramref name="x"/> ist gleich <paramref name="y"/>. Größer als 0 <paramref name="x"/> ist größer als <paramref name="y"/>.
		/// </returns>
		private int ComparePropertyLevel(int level, object x, object y, object xParent, object yParent)
		{
			if (x == null && y != null)
				return -1;
			else if (x != null && y == null)
				return 1;
			else if (x == null && y == null)
				return 0;

			if (!(x is IComparable))
				throw new ArgumentException("Parameter x doesn't implement IComparable.");
			if (!(y is IComparable))
				throw new ArgumentException("Parameter y doesn't implement IComparable.");

			int compared = 0;

			if (Sorts[level].SortDirection == ListSortDirection.Ascending)
				compared = ((IComparable)x).CompareTo(y);
			else if (Sorts[level].SortDirection == ListSortDirection.Descending)
				compared = ((IComparable)y).CompareTo(x);

			if (compared == 0)
				return CompareLevel(level + 1, xParent, yParent);
			else
				return compared;
		}

		#endregion
	}
}
